-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stop using loader functions, part 1 #1306
Stop using loader functions, part 1 #1306
Conversation
🦋 Changeset detectedLatest commit: 8601886 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
2a70cc1
to
fca7672
Compare
fca7672
to
2edb69f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is really great work!
|
||
export default function AssetsTable() { | ||
const balancesByAccount = useLoaderData() as BalancesByAccount[]; | ||
const balancesByAccount = useBalancesByAccount(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we were previously using abortLoader
to sanity-check that certain preconditions were met before proceeding with a data-fetching operation. If any of the checks failed, an exception was thrown, which aborted the loading process. Where is the equivalent of this happening in zquery
land?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should be unnecessary - the loader check was only present because the loaders were suppressing the check done at page layout
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've actually moved abortLoader
to root-router.tsx
.
@turbocrime are you saying I can remove them, then?
@@ -32,8 +32,12 @@ const retry = async (fn: () => boolean, ms = 500, rate = Math.max(ms / 10, 50)) | |||
* timeout. This is a temporary solution until loaders properly await Prax | |||
* connection. | |||
*/ | |||
export const abortLoader = async (): Promise<void> => { | |||
export const abortLoader = async (): Promise<null> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the trick, ie. returning null
, that enables instantaneous route changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, no — loaders are required to return something rather than void
. The instantaneous route changes are due to the fact that we no longer have loaders that are await
ing any slow requests.
|
||
export const rootRouter = createHashRouter([ | ||
export const rootRouter: Router = createHashRouter([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add some comments somewhere on the high-level similarities and differences between react-router loaders and zquery?
From what I can tell, both support:
- async data fetching
- state access to access fetched data in components
In terms of their differences, this is where some more clarification would be helpful. Zquery anchors on the idea of global state management, where state can be updated and shared across an application, right? React-router on the other hand fetches data specifically for a single route, and rather than being stored globally, it's only accessible by a specific component. I guess I'm wondering where component-level data fetching, a staple of the latter case, would generally be more preferred?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, hm, I'm realizing I should write up an ADR for all of this.
The idea is that we want to consolidate our state into one state machine, rather than having it split among several (Zustand, react-router, React Query, etc.).
I would assume that component-level data fetching as provided by React Query is for simpler applications that don't use a global data store like Zustand. It's also useful for server-side applications that load data and render it before sending HTML down to the browser — unlike what we're doing with our entirely client-side rendering.
export const { stakingTokensAndFilter, useStakingTokensAndFilter } = createZQuery({ | ||
name: 'stakingTokensAndFilter', | ||
fetch: async () => { | ||
const balancesByAccount = await getBalancesByAccount(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
random question out of curiosity, can you somehow chain zquery requests? for instance, instead of calling getBalancesByAccount()
, something like
// Access the state of balancesByAccount
const balances = useStore.getState().balances.balancesByAccount;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... That'd be an interesting use case. You could theoretically call useStore.getState().balances.balancesByAccount.revalidate()
, but at the moment, revalidate()
is a synchronous function that returns void
, and kicks off the request in the background, updating the state when the request completes.
We could consider adding some sort of "dependency" logic to ZQuery — i.e., when one ZQuery object updates its state, it re-triggers a fetch of another ZQuery object. 🤔 Do you have a use case in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't have specific use case in mind, was just curious if this was possible.
good to merge barring the resolution of conflicts ~ |
@@ -18,9 +20,11 @@ import { createIbcInSlice, IbcInSlice } from './ibc-in'; | |||
enableMapSet(); | |||
|
|||
export interface AllSlices { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add a comment on what slices generally are and what they represent in the context of the SliceCreator<SliceInterface>
custom type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a general comment here, but to be honest, I'm not super familiar with the typings and with why SliceCreator
exists the way it does 😬 (that was before my time)
e69f8f1
to
ca1b276
Compare
@jessepinho I still had a few questions I left as comments I was hoping you could answer, selfishly for my own benefit? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whoops, forgot to submit my comments
@@ -33,6 +33,7 @@ | |||
"@penumbra-zone/ui": "workspace:*", | |||
"@penumbra-zone/zquery": "workspace:*", | |||
"@radix-ui/react-icons": "^1.3.0", | |||
"@remix-run/router": "^1.16.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment in root-router
|
||
export const rootRouter = createHashRouter([ | ||
export const rootRouter: Router = createHashRouter([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding : Router
fixes a longtime TypeScript error we were getting in this file that for some reason didn't break the build.
export const AssetsLoader: LoaderFunction = async (): Promise<BalancesByAccount[]> => { | ||
await abortLoader(); | ||
return await getBalancesByAccount(); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I've deleted the loaders for a bunch of routes, but I've replaced their loaders in root-router.tsx
with abortLoader
. So, the abortLoader
functionality is still there; it's just not wrapped in a separate loader colocated with the component.
* A `ValueView` representing the address's balance of staking (UM) tokens. | ||
* Used to show the user how many tokens they have available to delegate. | ||
*/ | ||
unstakedTokens?: ValueView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prop was only used to pass it to <StakingActions />
below, so I've removed it now that unstakedTokens
can be accessed via the stakingTokensAndFilter
ZQuery object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to add some mocks to this file because the <StakingActions />
component no longer gets all its data via args, but instead also uses some ZQuery hooks that thus need to be mocked.
|
||
export default function AssetsTable() { | ||
const balancesByAccount = useLoaderData() as BalancesByAccount[]; | ||
const balancesByAccount = useBalancesByAccount(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've actually moved abortLoader
to root-router.tsx
.
@turbocrime are you saying I can remove them, then?
@@ -32,8 +32,12 @@ const retry = async (fn: () => boolean, ms = 500, rate = Math.max(ms / 10, 50)) | |||
* timeout. This is a temporary solution until loaders properly await Prax | |||
* connection. | |||
*/ | |||
export const abortLoader = async (): Promise<void> => { | |||
export const abortLoader = async (): Promise<null> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, no — loaders are required to return something rather than void
. The instantaneous route changes are due to the fact that we no longer have loaders that are await
ing any slow requests.
|
||
export const rootRouter = createHashRouter([ | ||
export const rootRouter: Router = createHashRouter([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, hm, I'm realizing I should write up an ADR for all of this.
The idea is that we want to consolidate our state into one state machine, rather than having it split among several (Zustand, react-router, React Query, etc.).
I would assume that component-level data fetching as provided by React Query is for simpler applications that don't use a global data store like Zustand. It's also useful for server-side applications that load data and render it before sending HTML down to the browser — unlike what we're doing with our entirely client-side rendering.
export const { stakingTokensAndFilter, useStakingTokensAndFilter } = createZQuery({ | ||
name: 'stakingTokensAndFilter', | ||
fetch: async () => { | ||
const balancesByAccount = await getBalancesByAccount(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... That'd be an interesting use case. You could theoretically call useStore.getState().balances.balancesByAccount.revalidate()
, but at the moment, revalidate()
is a synchronous function that returns void
, and kicks off the request in the background, updating the state when the request completes.
We could consider adding some sort of "dependency" logic to ZQuery — i.e., when one ZQuery object updates its state, it re-triggers a fetch of another ZQuery object. 🤔 Do you have a use case in mind?
@@ -18,9 +20,11 @@ import { createIbcInSlice, IbcInSlice } from './ibc-in'; | |||
enableMapSet(); | |||
|
|||
export interface AllSlices { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a general comment here, but to be honest, I'm not super familiar with the typings and with why SliceCreator
exists the way it does 😬 (that was before my time)
This PR goes through a bunch of routes where we're using react-router loaders, and replaces loader functionality with ZQuery. Note that this means some assumptions have changed about when data is available. react-router loaders wait to render their route components until data has loaded, which means your component can assume the data is already present. Since we're now effectively loading data from
useEffect()
(by way of ZQuery), we can't assume that data is present just because a component is rendered. So you'll see some instances of code that accounts forundefined
where we didn't previously have to.One of the benefits of this change is that route changes are instant. There's no lag time between when you click on a link to a route, and when the route changes, like there was before when route changes were blocked by loaders.
Before (note the delayed route changes after I click)
Screen.Recording.2024-06-17.at.3.45.06.PM.mov
After (note the instant route changes after I click)
Screen.Recording.2024-06-17.at.3.43.55.PM.mov
In this PR
balances
slice with abalancesByAccount
ZQuery object.transferableBalancesResponses
ZQuery object in the send state.shared
Zustand slice and added astakingTokenMetadata
ZQuery object to it, to be used by the send form and the IBC UIs.state.shared.stakingTokenMetadata
, and combine theaccountSwitcherFilter
andunstakedTokensByAccount
into a single ZQuery object, since they both depend on the same data.Future PRs
SwapLoader
andIbcLoader
to ZQuery. Those look a bit involved and this PR is already touching a lot of files, so I haven't done those yet.Relates to #1243